package com.vains.filter;

import com.vains.bean.Result;
import com.vains.constant.ErrorCodeConstants;
import com.vains.constant.GlobalConstants;
import com.vains.util.ServletUtils;
import lombok.extern.slf4j.Slf4j;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.oauth2.common.exceptions.OAuth2Exception;
import org.springframework.security.oauth2.provider.OAuth2Authentication;
import org.springframework.security.oauth2.provider.authentication.BearerTokenExtractor;
import org.springframework.security.oauth2.provider.authentication.TokenExtractor;
import org.springframework.security.oauth2.provider.token.TokenStore;
import org.springframework.security.web.authentication.www.BasicAuthenticationFilter;
import org.springframework.util.AntPathMatcher;

import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;

/**
 * 验证token 的过滤器
 *
 * @author vains
 * @date 2021/4/8 9:46
 */
@Slf4j
public class TokenAuthenticationFilter extends BasicAuthenticationFilter {

    private final TokenStore tokenStore;

    private static final TokenExtractor TOKEN_EXTRACTOR = new BearerTokenExtractor();

    public TokenAuthenticationFilter(AuthenticationManager authenticationManager, TokenStore tokenStore) {
        super(authenticationManager);
        this.tokenStore = tokenStore;
    }

    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws IOException, ServletException {
        extractToken(request, response, chain);
        // 清空security context 中存储的认证信息
        SecurityContextHolder.getContext().setAuthentication(null);
    }

    /**
     * 解析token并将认证信息放入 SecurityContextHolder 中
     * @param request 请求
     * @param response 响应
     * @param chain 过滤器链
     * @throws IOException 过滤器链执行失败时抛出
     * @throws ServletException 过滤器链执行失败时抛出
     */
    private void extractToken(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws IOException, ServletException {
        try {
            Authentication extract = TOKEN_EXTRACTOR.extract(request);
            if (extract == null) {
                String servletPath = request.getServletPath();
                if (permitUri(servletPath)) {
                    // 如果是授权码模式登陆，则继续执行，会被重定向到登录页面
                    chain.doFilter(request, response);
                } else {
                    // 否则就写回错误信息
                    OAuth2Exception oAuth2Exception = OAuth2Exception.create(OAuth2Exception.INVALID_TOKEN, "Full authentication is required to access this resource");
                    Result<OAuth2Exception> result = Result.oauth2Error(ErrorCodeConstants.INVALID_REQUEST_TOKEN, "Token异常：请携带token访问该接口.", oAuth2Exception);
                    ServletUtils.writeDataByResponse(response, result);
                }
                return;
            }
            OAuth2Authentication authentication = tokenStore.readAuthentication((String) extract.getPrincipal());
            if (authentication == null) {
                // Token不正确或已经过期
                String message = "Token异常：Token已经过期.";
                OAuth2Exception oAuth2Exception = OAuth2Exception.create(OAuth2Exception.INVALID_TOKEN, message);
                Result<OAuth2Exception> result = Result.oauth2Error(ErrorCodeConstants.INVALID_REQUEST_TOKEN, message, oAuth2Exception);
                ServletUtils.writeDataByResponse(response, result);
                return;
            }
            SecurityContextHolder.getContext().setAuthentication(authentication);
            log.info("token解析成功.");
        } catch (Exception failed) {
            SecurityContextHolder.clearContext();
            log.warn("token解析失败.");
        }
        chain.doFilter(request, response);
    }

    /**
     * 是否是需要放行的url
     * @param servletPath 当前请求路径
     * @return true=是需要放行的路径 false=不是
     */
    private boolean permitUri(String servletPath) {
        AntPathMatcher matcher = new AntPathMatcher();
        // 遍历判断是否是需要放行的url
        for (String permit : GlobalConstants.AUTH_WHITE_LIST) {
            if (matcher.match(permit, servletPath)) {
                return true;
            }
        }
        // 如果都不是，判断是否是授权码地址
        return matcher.match(GlobalConstants.AUTHORIZATION_CODE_URI, servletPath);
    }

}
